using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Transactions;
using Microsoft.Practices.ServiceLocation;
using NQueries.Utils;

namespace NQueries
{
    /// <inheritdoc />
    public sealed class UnitOfWork : IUnitOfWork
    {
        [ThreadStatic]
        static Stack<IUnitOfWork> _UnitOfWorkQueue;

        static readonly object _Lock = new object();
        readonly DateTime _CreateDate;
        readonly bool _IsDataContextOwner;
        bool _IsDisposed;

        UnitOfWork(string name, IDataContext dataContext, DependentTransaction dependentTransaction, TransactionScope transactionScope)
        {
            Name = name;

            lock (_Lock)
            {
                if (_UnitOfWorkQueue == null)
                    _UnitOfWorkQueue = new Stack<IUnitOfWork>();
                _UnitOfWorkQueue.Push(this);
            }

            if (dataContext == null)
            {
                DataContext = ServiceLocator.Current.GetInstance<IDataContext>();
                _IsDataContextOwner = true;
            }
            else
                DataContext = dataContext;

            DependentTransaction = dependentTransaction;
            TransactionScope = transactionScope;

            if (OnUnitOfWorkCreated != null)
                OnUnitOfWorkCreated(this, new UnitOfWorkCreatedEventArgs(Name));

            _CreateDate = DateTime.UtcNow;
        }

        /// <summary>
        /// Gets the current <see cref="IDataContext"/>.
        /// </summary>
        public static IDataContext CurrentDataContext
        {
            get
            {
                lock (_Lock)
                    return _UnitOfWorkQueue != null && _UnitOfWorkQueue.Count > 0 ? ((UnitOfWork)_UnitOfWorkQueue.Peek()).DataContext : null;
            }
        }

        TransactionScope TransactionScope { get; set; }
        DependentTransaction DependentTransaction { get; set; }

        #region IUnitOfWork Members

        /// <summary>
        /// Disposes the object.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <inheritdoc />
        public IDataContext DataContext { get; private set; }

        /// <inheritdoc />
        public string Name { get; private set; }

        /// <inheritdoc />
        public int Commit()
        {
            int result = DataContext.SaveChanges();
            if (TransactionScope != null)
            {
                TransactionScope.Complete();
                TransactionScope.Dispose();
                TransactionScope = null;
            }
            if (DependentTransaction != null)
            {
                DependentTransaction.Complete();
                DependentTransaction.Dispose();
                DependentTransaction = null;
            }
            return result;
        }

        /// <inheritdoc />
        public DependentTransaction GetDependentTransaction(DependentCloneOption dependentCloneOption)
        {
            if (Transaction.Current == null)
                throw new NQueriesException("cant get dependent transaction: no current transaction started.");
            return Transaction.Current.DependentClone(dependentCloneOption);
        }

        #endregion

        /// <summary>
        /// Is called, whenever a unit of work is created.
        /// </summary>
        public static event EventHandler<UnitOfWorkCreatedEventArgs> OnUnitOfWorkCreated;

        /// <summary>
        /// Is called, just before a unit of work gets disposed.
        /// </summary>
        public static event EventHandler<UnitOfWorkDisposedEventArgs> OnUnitOfWorkDisposed;

        /// <summary>
        /// Destructor.
        /// </summary>
        ~UnitOfWork() { Dispose(false); }

        /// <summary>
        /// Starts a new unit of work.
        /// </summary>
        /// <returns></returns>
        public static IUnitOfWork Begin() { return new UnitOfWork(string.Empty, null, null, null); }

        /// <summary>
        /// Starts a unit of work.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public static IUnitOfWork Begin(string name) { return new UnitOfWork(name, null, null, null); }

        /// <summary>
        /// Starts a unit of work.
        /// </summary>
        /// <param name="transactionScopeOption"></param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        public static IUnitOfWork Begin(TransactionScopeOption transactionScopeOption) { return new UnitOfWork(string.Empty, null, null, new TransactionScope(transactionScopeOption)); }

        /// <summary>
        /// Starts a unit of work.
        /// </summary>
        /// <param name="transactionScopeOption"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        public static IUnitOfWork Begin(TransactionScopeOption transactionScopeOption, string name) { return new UnitOfWork(name, null, null, new TransactionScope(transactionScopeOption)); }

        /// <summary>
        /// Starts a unit of work.
        /// </summary>
        /// <param name="dependentTransaction"></param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        public static IUnitOfWork Begin(DependentTransaction dependentTransaction) { return new UnitOfWork(string.Empty, null, dependentTransaction, new TransactionScope(dependentTransaction)); }

        /// <summary>
        /// Starts a unit of work.
        /// </summary>
        /// <param name="dependentTransaction"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        public static IUnitOfWork Begin(DependentTransaction dependentTransaction, string name) { return new UnitOfWork(name, null, dependentTransaction, new TransactionScope(dependentTransaction)); }

        /// <summary>
        /// Starts a new unit of work and tries to use a existing data context from the current thread.
        /// If no datacontext is present, a new one will be created.
        /// </summary>
        /// <returns></returns>
        public static IUnitOfWork BeginOrJoin()
        {
            lock (_Lock)
                return CurrentDataContext != null ? new UnitOfWork(string.Empty, CurrentDataContext, null, null) : Begin();
        }

        /// <summary>
        /// Starts a new unit of work and tries to use a existing data context from the current thread.
        /// If no datacontext is present, a new one will be created.
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public static IUnitOfWork BeginOrJoin(string name)
        {
            lock (_Lock)
                return CurrentDataContext != null ? new UnitOfWork(name, CurrentDataContext, null, null) : Begin(name);
        }

        /// <summary>
        /// Starts a new unit of work and tries to use a existing data context from the current thread.
        /// If no datacontext is present, a new one will be created.
        /// </summary>
        /// <param name="transactionScopeOption"></param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        public static IUnitOfWork BeginOrJoin(TransactionScopeOption transactionScopeOption)
        {
            lock (_Lock)
                return CurrentDataContext != null ? new UnitOfWork(string.Empty, CurrentDataContext, null, new TransactionScope(transactionScopeOption)) : Begin(transactionScopeOption);
        }

        /// <summary>
        /// Starts a new unit of work and tries to use a existing data context from the current thread.
        /// If no datacontext is present, a new one will be created.
        /// </summary>
        /// <param name="transactionScopeOption"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        public static IUnitOfWork BeginOrJoin(TransactionScopeOption transactionScopeOption, string name)
        {
            lock (_Lock)
                return CurrentDataContext != null ? new UnitOfWork(name, CurrentDataContext, null, new TransactionScope(transactionScopeOption)) : Begin(transactionScopeOption, name);
        }

        /// <summary>
        /// Starts a new unit of work and tries to use a existing data context from the current thread.
        /// If no datacontext is present, a new one will be created.
        /// </summary>
        /// <param name="dependentTransaction"></param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        public static IUnitOfWork BeginOrJoin(DependentTransaction dependentTransaction)
        {
            lock (_Lock)
                return CurrentDataContext != null ? new UnitOfWork(string.Empty, CurrentDataContext, dependentTransaction, new TransactionScope(dependentTransaction)) : Begin(dependentTransaction);
        }

        /// <summary>
        /// Starts a new unit of work and tries to use a existing data context from the current thread.
        /// If no datacontext is present, a new one will be created.
        /// </summary>
        /// <param name="dependentTransaction"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        public static IUnitOfWork BeginOrJoin(DependentTransaction dependentTransaction, string name)
        {
            lock (_Lock)
                return CurrentDataContext != null ? new UnitOfWork(name, CurrentDataContext, dependentTransaction, new TransactionScope(dependentTransaction)) : Begin(dependentTransaction, name);
        }

        /// <inheritdoc />
        void Dispose(bool disposing)
        {
            if (!_IsDisposed)
            {
                if (disposing)
                {
                    if (TransactionScope != null)
                        TransactionScope.Dispose();
                    if (DependentTransaction != null)
                        DependentTransaction.Dispose();
                    if (_IsDataContextOwner && DataContext != null)
                        DataContext.Dispose();
                    TransactionScope = null;
                    DependentTransaction = null;
                    DataContext = null;
                }

                lock (_Lock)
                    if (_UnitOfWorkQueue != null)
                        _UnitOfWorkQueue.Pop();
                _IsDisposed = true;

                if (OnUnitOfWorkDisposed != null)
                    OnUnitOfWorkDisposed(this, new UnitOfWorkDisposedEventArgs(Name, DateTime.UtcNow - _CreateDate));
            }
        }

        /// <inheritdoc />
        public override string ToString() { return "{0} name: <{1}>".FormatWith(base.ToString(), Name); }

        // TODO: what to do with MSDTC
        ///// <summary>
        ///// Starts the distributed transaction coordinator service that is requiered by the System.Transactions namespace.
        ///// </summary>
        //static void StartMsdtc()
        //{
        //    try
        //    {
        //        using (ServiceController service = new ServiceController("MSDTC"))
        //        {
        //            if (service.Status != ServiceControllerStatus.Running)
        //                service.Start();

        //            service.WaitForStatus(ServiceControllerStatus.Running, new TimeSpan(0, 0, 1));
        //        }
        //    }
        //    catch (Exception e)
        //    {
        //        throw new DataException("could not start MSDTC service", e);
        //    }
        //}
    }
}